package server

/*******************************************************************************
Copyright:cloud
Author:cloudapex@126.com
Version:1.0
Date:2020-06-12
Description: api server
*******************************************************************************/
import (
	"context"
	"encoding/json"
	"fmt"
	"log"
	"net"
	"permis/api/api"
	"permis/api/login"
	"permis/api/sys"
	"permis/api/tool"
	"permis/conf"
	"permis/model"
	"reflect"
	"time"

	permit "gitee.com/cloudapex/permit"
	gpb "gitee.com/cloudapex/protos/permis/pb"
	"gitee.com/cloudapex/ulern/cto"
	"gitee.com/cloudapex/ulern/htp"
	"gitee.com/cloudapex/ulern/util"

	grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware"
	grpc_auth "github.com/grpc-ecosystem/go-grpc-middleware/auth"
	grpc_recovery "github.com/grpc-ecosystem/go-grpc-middleware/recovery"
	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/credentials"
	"google.golang.org/grpc/keepalive"
	"google.golang.org/grpc/status"
)

func init() { cto.Install("MC_Serve", &server{}) }

type server struct {
	cto.ControlBase

	gSer *grpc.Server
}

func (this *server) HandleInit() {
	this.ControlBase.HandleInit()

	permit.UseRdb(model.RDB(model.ERDB_Permt)) // 初始化权限缓存使用的redis连接

	this.startServer()
}
func (this *server) HandleTerm() {
	util.Cast(this.gSer != nil, func() { this.gSer.Stop() }, nil)

	this.ControlBase.HandleTerm()
}

// ------------ internal
func (this *server) startServer() {

	lis, err := net.Listen("tcp", conf.Main.ListenAddr())
	if err != nil {
		log.Fatalf("failed to listen: %v", err)
	}

	gs := grpc.NewServer(this.initOptions()...)

	gpb.RegisterLoginServer(gs, login.GServer())
	gpb.RegisterSystemServer(gs, sys.GServer())
	gpb.RegisterToolServer(gs, tool.GServer())

	go func() {
		if err := gs.Serve(lis); err != nil {
			this.Fatal("Grpc Serve err:%v", err)
		}
	}()

	this.gSer = gs
	util.Log.Info("Start Grpc Serve on listen addr:%v", conf.Main.ListenAddr())
}
func (this *server) initOptions() []grpc.ServerOption {
	var opts []grpc.ServerOption

	// tls
	gsOpt := conf.Main.GSerOption()
	if gsOpt.Tls.Enable {
		creds, err := credentials.NewServerTLSFromFile(util.ExePathJoin(gsOpt.Tls.CrtFile), util.ExePathJoin(gsOpt.Tls.KeyFile))
		if err != nil {
			log.Fatalf("Failed to generate credentials %v", err)
		}
		opts = []grpc.ServerOption{grpc.Creds(creds)}
	}

	// keepalive policy
	var kaep = keepalive.EnforcementPolicy{
		MinTime:             5 * time.Second, // If a client pings more than once every 5 seconds, terminate the connection
		PermitWithoutStream: true,            // Allow pings even when there are no active streams
	}
	opts = append(opts, grpc.KeepaliveEnforcementPolicy(kaep))

	// keepalive params
	var kasp = keepalive.ServerParameters{
		MaxConnectionIdle:     15 * time.Second, // If a client is idle for 15 seconds, send a GOAWAY
		MaxConnectionAge:      30 * time.Second, // If any connection is alive for more than 30 seconds, send a GOAWAY
		MaxConnectionAgeGrace: 5 * time.Second,  // Allow 5 seconds for pending RPCs to complete before forcibly closing connections
		Time:                  5 * time.Second,  // Ping the client if it is idle for 5 seconds to ensure the connection is still active
		Timeout:               1 * time.Second,  // Wait 1 second for the ping ack before assuming the connection is dead
	}
	opts = append(opts, grpc.KeepaliveParams(kasp))

	// interceptors
	intors := []grpc.UnaryServerInterceptor{this.errorStatusInterceptor}
	util.Cast(gsOpt.RunMode == "debug", func() { intors = append(intors, this.debugLoggerInterceptor) }, nil)
	intors = append(intors, grpc_recovery.UnaryServerInterceptor(this.recoveryHandler())) // recovery
	util.Cast(gsOpt.Cred.Enable, func() { intors = append(intors, grpc_auth.UnaryServerInterceptor(this.authInterceptor)) }, nil)
	opts = append(opts, grpc.UnaryInterceptor(grpc_middleware.ChainUnaryServer(intors...)))

	return opts
}
func (this *server) authInterceptor(ctx context.Context) (context.Context, error) {
	token, err := grpc_auth.AuthFromMD(ctx, conf.Main.GSerOptCred().AuthScheme) // `bearer token` 形式
	if err != nil {
		return nil, err
	}

	tokenInfo := &model.UserTokenInfo{}
	if err := json.Unmarshal([]byte(token), tokenInfo); err != nil {
		return nil, status.Errorf(codes.Unauthenticated, "invalid auth token: %v", err)
	}

	if !tokenInfo.Valid() {
		return nil, status.Errorf(codes.DataLoss, "auth token info loss: %v", err)
	}

	return context.WithValue(ctx, htp.DEF_CLAIMS_HEAD_KEY, tokenInfo), nil
}

func (this *server) errorStatusInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
	m, err := handler(ctx, req)
	if err != nil {
		s, _ := status.FromError(err)
		util.Log.Error("[\"GRPC-Response\"] %s {%+v} -> {code: %d, msg: %s, err: %s}", info.FullMethod, req, s.Code(), s.Message(), s.Details())
	} else if reflect.ValueOf(m).IsNil() {
		util.Log.Error("[\"GRPC-Response\"] %s {%+v} -> %s", info.FullMethod, req, "grpc: error while encode: proto: Marshal called with nil")
	}

	return m, err
}

func (this *server) debugLoggerInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
	defer func(begin time.Time) {
		util.Log.Debug("[\"GRPC-Request\"] | %s | %s | {%+v}", info.FullMethod, time.Since(begin), req)
	}(time.Now())

	return handler(ctx, req)
}
func (this *server) recoveryHandler() grpc_recovery.Option {
	return grpc_recovery.WithRecoveryHandler(func(x interface{}) error {
		util.Catch("grpc", x)
		return api.Error(codes.Internal, "系统异常", fmt.Errorf("%s", x))
	})
}
